/**
 * Copyright (C) 2014-2019 by Wen Yu.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * <p>
 * Any modifications to this file must keep this entire header intact.
 * <p>
 * Change History - most recent changes go on top of previous changes
 * <p>
 * TIFFMeta.java
 * <p>
 * Who   Date       Description
 * ====  =========  =====================================================================
 * WY    21Jun2019  Added code for removeMetadata to return the removed metadata as a map
 * WY    14May2019  Write IPTC to normal TIFF IPTC tag instead of PhotoShop IRB block
 * WY    06Jul2015  Added insertXMP(InputSream, OutputStream, XMP)
 * WY    21Jun2015  Removed copyright notice from generated TIFF images
 * WY    15Apr2015  Changed the argument type for insertIPTC() and insertIRB()
 * WY    07Apr2015  Removed insertICCProfile() AWT related code
 * WY    07Apr2015  Merge Adobe IRB IPTC and TIFF IPTC data if both exist
 * WY    13Mar2015  Initial creation
 */

package com.symaster.common.pixy.meta.tiff;

import com.symaster.common.pixy.image.jpeg.Marker;
import com.symaster.common.pixy.image.tiff.*;
import com.symaster.common.pixy.io.*;
import com.symaster.common.pixy.meta.Metadata;
import com.symaster.common.pixy.meta.MetadataType;
import com.symaster.common.pixy.meta.adobe.*;
import com.symaster.common.pixy.meta.exif.Exif;
import com.symaster.common.pixy.meta.exif.ExifTag;
import com.symaster.common.pixy.meta.exif.GPSTag;
import com.symaster.common.pixy.meta.exif.InteropTag;
import com.symaster.common.pixy.meta.icc.ICCProfile;
import com.symaster.common.pixy.meta.image.Comments;
import com.symaster.common.pixy.meta.iptc.IPTC;
import com.symaster.common.pixy.meta.iptc.IPTCDataSet;
import com.symaster.common.pixy.meta.iptc.IPTCTag;
import com.symaster.common.pixy.meta.xmp.XMP;
import com.symaster.common.pixy.string.StringUtils;
import com.symaster.common.pixy.string.XMLUtils;
import com.symaster.common.util.ArrayUtils;
import com.symaster.common.util.MetadataUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;

import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class TIFFMeta {
    // Offset where to write the value of the first IFD offset
    public static final int OFFSET_TO_WRITE_FIRST_IFD_OFFSET = 0x04;
    public static final int FIRST_WRITE_OFFSET = 0x08;
    public static final int STREAM_HEAD = 0x00;

    // Obtain a logger instance
    private static final Logger LOGGER = LoggerFactory.getLogger(TIFFMeta.class);

    private static int copyHeader(RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        rin.seek(STREAM_HEAD);
        // First 2 bytes determine the byte order of the file, "MM" or "II"
        short endian = rin.readShort();

        if (endian == IOUtils.BIG_ENDIAN) {
            rin.setReadStrategy(ReadStrategyMM.getInstance());
            rout.setWriteStrategy(WriteStrategyMM.getInstance());
        } else if (endian == IOUtils.LITTLE_ENDIAN) {
            rin.setReadStrategy(ReadStrategyII.getInstance());
            rout.setWriteStrategy(WriteStrategyII.getInstance());
        } else {
            rin.close();
            rout.close();
            throw new RuntimeException("Invalid TIFF byte order");
        }

        rout.writeShort(endian);
        // Read TIFF identifier
        rin.seek(0x02);
        short tiff_id = rin.readShort();

        if (tiff_id != 0x2a)//"*" 42 decimal
        {
            rin.close();
            rout.close();
            throw new RuntimeException("Invalid TIFF identifier");
        }

        rout.writeShort(tiff_id);
        rin.seek(OFFSET_TO_WRITE_FIRST_IFD_OFFSET);

        return rin.readInt();
    }

    private static Collection<IPTCDataSet> copyIPTCDataSet(Collection<IPTCDataSet> iptcs, byte[] data) throws IOException {
        IPTC iptc = new IPTC(data);
        // Shallow copy the map
        Map<IPTCTag, List<IPTCDataSet>> dataSetMap = new HashMap<IPTCTag, List<IPTCDataSet>>(iptc.getDataSets());
        for (IPTCDataSet set : iptcs)
            if (!set.allowMultiple())
                dataSetMap.remove(set.getName());
        for (List<IPTCDataSet> iptcList : dataSetMap.values())
            iptcs.addAll(iptcList);

        return iptcs;
    }

    private static TiffField<?> copyJpegHufTable(RandomAccessInputStream rin, RandomAccessOutputStream rout, TiffField<?> field, int curPos) throws IOException {
        int[] data = field.getDataAsLong();
        int[] tmp = new int[data.length];

        for (int i = 0; i < data.length; i++) {
            rin.seek(data[i]);
            tmp[i] = curPos;
            byte[] htable = new byte[16];
            IOUtils.readFully(rin, htable);
            IOUtils.write(rout, htable);
            curPos += 16;

            int numCodes = 0;

            for (int j = 0; j < 16; j++) {
                numCodes += htable[j] & 0xff;
            }

            curPos += numCodes;

            htable = new byte[numCodes];
            IOUtils.readFully(rin, htable);
            IOUtils.write(rout, htable);
        }

        if (TiffTag.fromShort(field.getTag()) == TiffTag.JPEG_AC_TABLES)
            return new LongField(TiffTag.JPEG_AC_TABLES.getValue(), tmp);

        return new LongField(TiffTag.JPEG_DC_TABLES.getValue(), tmp);
    }

    private static void copyJpegIFByteCount(RandomAccessInputStream rin, RandomAccessOutputStream rout, int offset, int outOffset) throws IOException {
        boolean finished = false;
        int length = 0;
        short marker;
        Marker emarker;

        rin.seek(offset);
        rout.seek(outOffset);
        // The very first marker should be the start_of_image marker!
        if (Marker.fromShort(IOUtils.readShortMM(rin)) != Marker.SOI) {
            return;
        }

        IOUtils.writeShortMM(rout, Marker.SOI.getValue());

        marker = IOUtils.readShortMM(rin);

        while (!finished) {
            if (Marker.fromShort(marker) == Marker.EOI) {
                IOUtils.writeShortMM(rout, marker);
                finished = true;
            } else { // Read markers
                emarker = Marker.fromShort(marker);

                switch (emarker) {
                    case JPG: // JPG and JPGn shouldn't appear in the image.
                    case JPG0:
                    case JPG13:
                    case TEM: // The only stand alone mark besides SOI, EOI, and RSTn.
                        marker = IOUtils.readShortMM(rin);
                        break;
                    case SOS:
                        marker = copyJpegSOS(rin, rout);
                        break;
                    case PADDING:
                        int nextByte = 0;
                        while ((nextByte = rin.read()) == 0xff) {
						}
                        marker = (short) ((0xff << 8) | nextByte);
                        break;
                    default:
                        length = IOUtils.readUnsignedShortMM(rin);
                        byte[] buf = new byte[length - 2];
                        rin.read(buf);
                        IOUtils.writeShortMM(rout, marker);
                        IOUtils.writeShortMM(rout, length);
                        rout.write(buf);
                        marker = IOUtils.readShortMM(rin);
                }
            }
        }
    }

    private static TiffField<?> copyJpegQTable(RandomAccessInputStream rin, RandomAccessOutputStream rout, TiffField<?> field, int curPos) throws IOException {
        byte[] qtable = new byte[64];
        int[] data = field.getDataAsLong();
        int[] tmp = new int[data.length];

        for (int i = 0; i < data.length; i++) {
            rin.seek(data[i]);
            tmp[i] = curPos;
            IOUtils.readFully(rin, qtable);
            IOUtils.write(rout, qtable);
            curPos += 64;
        }

        return new LongField(TiffTag.JPEG_Q_TABLES.getValue(), tmp);
    }

    private static short copyJpegSOS(RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        int len = IOUtils.readUnsignedShortMM(rin);
        byte[] buf = new byte[len - 2];
        IOUtils.readFully(rin, buf);
        IOUtils.writeShortMM(rout, Marker.SOS.getValue());
        IOUtils.writeShortMM(rout, len);
        rout.write(buf);
        // Actual image data follow.
        int nextByte = 0;
        short marker = 0;

        while ((nextByte = IOUtils.read(rin)) != -1) {
            rout.write(nextByte);

            if (nextByte == 0xff) {
                nextByte = IOUtils.read(rin);
                rout.write(nextByte);

                if (nextByte == -1) {
                    throw new IOException("Premature end of SOS segment!");
                }

                if (nextByte != 0x00) {
                    marker = (short) ((0xff << 8) | nextByte);

                    switch (Marker.fromShort(marker)) {
                        case RST0:
                        case RST1:
                        case RST2:
                        case RST3:
                        case RST4:
                        case RST5:
                        case RST6:
                        case RST7:
                            continue;
                        default:
                    }
                    break;
                }
            }
        }

        if (nextByte == -1) {
            throw new IOException("Premature end of SOS segment!");
        }

        return marker;
    }

    /**
     * @param offset offset to write page image data
     *
     * @return the position where to write the IFD for the current image page
     */
    private static int copyPageData(IFD ifd, int offset, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        int writeOffset = offset; // We copy the offset to a local variable to keep the original value
        int writeByteCount = 0; // To fix JPEG data double copy issue

        // Move stream pointer to the right place
        rout.seek(writeOffset);

        // Original image data start from these offsets.
        TiffField<?> stripOffSets = ifd.removeField(TiffTag.STRIP_OFFSETS);

        if (stripOffSets == null)
            stripOffSets = ifd.removeField(TiffTag.TILE_OFFSETS);

        TiffField<?> stripByteCounts = ifd.getField(TiffTag.STRIP_BYTE_COUNTS);

        if (stripByteCounts == null)
            stripByteCounts = ifd.getField(TiffTag.TILE_BYTE_COUNTS);
        /*
         * Make sure this will work in the case when neither STRIP_OFFSETS nor TILE_OFFSETS presents.
         * Not sure if this will ever happen for TIFF. JPEG EXIF data do not contain these fields.
         */
        if (stripOffSets != null) {
            int[] counts = stripByteCounts.getDataAsLong();
            int[] off = stripOffSets.getDataAsLong();
            int[] temp = new int[off.length];

            TiffField<?> tiffField = ifd.getField(TiffTag.COMPRESSION);

            // Uncompressed image with one strip or tile (may contain wrong StripByteCounts value)
            // Bug fix for uncompressed image with one strip and wrong StripByteCounts value
            if ((tiffField == null) || (tiffField != null && tiffField.getDataAsLong()[0] == 1)) { // Uncompressed data
                int planaryConfiguration = 1;

                tiffField = ifd.getField(TiffTag.PLANAR_CONFIGURATTION);
                if (tiffField != null) planaryConfiguration = tiffField.getDataAsLong()[0];

                tiffField = ifd.getField(TiffTag.SAMPLES_PER_PIXEL);

                int samplesPerPixel = 1;
                if (tiffField != null) samplesPerPixel = tiffField.getDataAsLong()[0];

                // If there is only one strip/samplesPerPixel strips for PlanaryConfiguration = 2
                if ((planaryConfiguration == 1 && off.length == 1) || (planaryConfiguration == 2 && off.length == samplesPerPixel)) {
                    int[] totalBytes2Read = getBytes2Read(ifd);

                    for (int i = 0; i < off.length; i++)
                        counts[i] = totalBytes2Read[i];
                }
            } // End of bug fix

            writeByteCount = counts[0];

            // We are going to write the image data first
            rout.seek(writeOffset);

            // Copy image data from offset
            for (int i = 0; i < off.length; i++) {
                rin.seek(off[i]);
                byte[] buf = new byte[counts[i]];
                rin.readFully(buf);
                rout.write(buf);
                temp[i] = writeOffset;
                writeOffset += buf.length;
            }

            if (ifd.getField(TiffTag.STRIP_BYTE_COUNTS) != null)
                ifd.addField(new LongField(TiffTag.STRIP_OFFSETS.getValue(), temp));
            else
                ifd.addField(new LongField(TiffTag.TILE_OFFSETS.getValue(), temp));
        }

        // Add software field.
        String softWare = "ICAFE - https://github.com/dragon66/icafe\0";
        ifd.addField(new ASCIIField(TiffTag.SOFTWARE.getValue(), softWare));

        /* The following are added to work with old-style JPEG compression (type 6) */
        /* One of the flavors (found in JPEG EXIF thumbnail IFD - IFD1) of the old JPEG compression contains this field */
        TiffField<?> jpegIFOffset = ifd.removeField(TiffTag.JPEG_INTERCHANGE_FORMAT);
        if (jpegIFOffset != null) {
            TiffField<?> jpegIFByteCount = ifd.removeField(TiffTag.JPEG_INTERCHANGE_FORMAT_LENGTH);
            if (jpegIFOffset.getDataAsLong()[0] != stripOffSets.getDataAsLong()[0]) {
                try {
                    if (jpegIFByteCount != null) {
                        rin.seek(jpegIFOffset.getDataAsLong()[0]);
                        byte[] bytes2Read = new byte[jpegIFByteCount.getDataAsLong()[0]];
                        rin.readFully(bytes2Read);
                        rout.seek(writeOffset);
                        rout.write(bytes2Read);
                        ifd.addField(jpegIFByteCount);
                    } else {
                        long startOffset = rout.getStreamPointer();
                        copyJpegIFByteCount(rin, rout, jpegIFOffset.getDataAsLong()[0], writeOffset);
                        long endOffset = rout.getStreamPointer();
                        ifd.addField(new LongField(TiffTag.JPEG_INTERCHANGE_FORMAT_LENGTH.getValue(), new int[]{(int) (endOffset - startOffset)}));
                    }
                    jpegIFOffset = new LongField(TiffTag.JPEG_INTERCHANGE_FORMAT.getValue(), new int[]{writeOffset});
                    ifd.addField(jpegIFOffset);
                } catch (EOFException ex) {
				}
			} else { // To fix the issue of double copy the JPEG data, we can safely re-assign the pointers.
                ifd.addField(new LongField(TiffTag.JPEG_INTERCHANGE_FORMAT.getValue(), new int[]{offset}));
                ifd.addField(new LongField(TiffTag.JPEG_INTERCHANGE_FORMAT_LENGTH.getValue(), new int[]{writeByteCount}));
            }
        }
        /* Another flavor of the old style JPEG compression type 6 contains separate tables */
        TiffField<?> jpegTable = ifd.removeField(TiffTag.JPEG_DC_TABLES);
        if (jpegTable != null) {
            try {
                ifd.addField(copyJpegHufTable(rin, rout, jpegTable, (int) rout.getStreamPointer()));
            } catch (EOFException ex) {
			}
        }

        jpegTable = ifd.removeField(TiffTag.JPEG_AC_TABLES);
        if (jpegTable != null) {
            try {
                ifd.addField(copyJpegHufTable(rin, rout, jpegTable, (int) rout.getStreamPointer()));
            } catch (EOFException ex) {
			}
        }

        jpegTable = ifd.removeField(TiffTag.JPEG_Q_TABLES);
        if (jpegTable != null) {
            try {
                ifd.addField(copyJpegQTable(rin, rout, jpegTable, (int) rout.getStreamPointer()));
            } catch (EOFException ex) {
			}
        }
        /* End of code to work with old-style JPEG compression */

        // Return the actual stream position (we may have lost track of it)
        return (int) rout.getStreamPointer();
    }

    // Copy a list of IFD and associated image data if any
    private static int copyPages(List<IFD> list, int writeOffset, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        // Write the first page data
        writeOffset = copyPageData(list.get(0), writeOffset, rin, rout);
        // Then write the first IFD
        writeOffset = list.get(0).write(rout, writeOffset);
        // We are going to write the remaining image pages and IFDs if any
        for (int i = 1; i < list.size(); i++) {
            writeOffset = copyPageData(list.get(i), writeOffset, rin, rout);
            // Tell the IFD to update next IFD offset for the following IFD
            list.get(i - 1).setNextIFDOffset(rout, writeOffset);
            writeOffset = list.get(i).write(rout, writeOffset);
        }

        return writeOffset;
    }

    /**
     * Extracts ICC_Profile from certain page of TIFF if any
     *
     * @param pageNumber page number from which to extract ICC_Profile
     * @param rin RandomAccessInputStream for the input TIFF
     * @return a byte array for the extracted ICC_Profile or null if none exists
     * @throws Exception
     */
    public static byte[] extractICCProfile(int pageNumber, RandomAccessInputStream rin) throws Exception {
        // Read pass image header
        int offset = readHeader(rin);
        // Read the IFDs into a list first
        List<IFD> ifds = new ArrayList<IFD>();
        readIFDs(null, null, TiffTag.class, ifds, offset, rin);

        if (pageNumber < 0 || pageNumber >= ifds.size())
            throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));

        IFD workingPage = ifds.get(pageNumber);
        TiffField<?> f_iccProfile = workingPage.getField(TiffTag.ICC_PROFILE);
        if (f_iccProfile != null) {
            return (byte[]) f_iccProfile.getData();
        }

        return null;
    }

    public static byte[] extractICCProfile(RandomAccessInputStream rin) throws Exception {
        return extractICCProfile(0, rin);
    }

    public static IRBThumbnail extractThumbnail(int pageNumber, RandomAccessInputStream rin) throws IOException {
        // Read pass image header
        int offset = readHeader(rin);
        // Read the IFDs into a list first
        List<IFD> ifds = new ArrayList<IFD>();
        readIFDs(null, null, TiffTag.class, ifds, offset, rin);

        if (pageNumber < 0 || pageNumber >= ifds.size())
            throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));

        IFD workingPage = ifds.get(pageNumber);
        TiffField<?> f_photoshop = workingPage.getField(TiffTag.PHOTOSHOP);
        if (f_photoshop != null) {
            byte[] data = (byte[]) f_photoshop.getData();
            IRB irb = new IRB(data);
            if (irb.containsThumbnail()) {
                IRBThumbnail thumbnail = irb.getThumbnail();
                return thumbnail;
            }
        }

        return null;
    }

    public static IRBThumbnail extractThumbnail(RandomAccessInputStream rin) throws IOException {
        return extractThumbnail(0, rin);
    }

    public static void extractThumbnail(RandomAccessInputStream rin, String pathToThumbnail) throws IOException {
        IRBThumbnail thumbnail = extractThumbnail(rin);
        if (thumbnail != null) {
            String outpath = "";
            if (pathToThumbnail.endsWith("\\") || pathToThumbnail.endsWith("/"))
                outpath = pathToThumbnail + "photoshop_thumbnail.jpg";
            else
                outpath = pathToThumbnail.replaceFirst("[.][^.]+$", "") + "_photoshop_t.jpg";
            FileOutputStream fout = new FileOutputStream(outpath);
            if (thumbnail.getDataType() == IRBThumbnail.DATA_TYPE_KJpegRGB) {
                fout.write(thumbnail.getCompressedImage());
            } else {
                MetadataUtils.saveAsJPEG(thumbnail.getRawImage(), fout, 100);
            }
            fout.close();
        }
    }

    // Used to calculate how many bytes to read in case we have only one strip or tile
    private static int[] getBytes2Read(IFD ifd) {
        // Let's calculate how many bytes we are supposed to read
        TiffField<?> tiffField = ifd.getField(TiffTag.IMAGE_WIDTH);
        int imageWidth = tiffField.getDataAsLong()[0];
        tiffField = ifd.getField(TiffTag.IMAGE_LENGTH);
        int imageHeight = tiffField.getDataAsLong()[0];

        // For YCbCr image only
        int horizontalSampleFactor = 2; // Default 2X2
        int verticalSampleFactor = 2; // Not 1X1

        int photoMetric = ifd.getField(TiffTag.PHOTOMETRIC_INTERPRETATION).getDataAsLong()[0];

        // Correction for imageWidth and imageHeight for YCbCr image
        if (photoMetric == TiffFieldEnum.PhotoMetric.YCbCr.getValue()) {
            TiffField<?> f_YCbCrSubSampling = ifd.getField(TiffTag.YCbCr_SUB_SAMPLING);

            if (f_YCbCrSubSampling != null) {
                int[] sampleFactors = f_YCbCrSubSampling.getDataAsLong();
                horizontalSampleFactor = sampleFactors[0];
                verticalSampleFactor = sampleFactors[1];
            }
            imageWidth = ((imageWidth + horizontalSampleFactor - 1) / horizontalSampleFactor) * horizontalSampleFactor;
            imageHeight = ((imageHeight + verticalSampleFactor - 1) / verticalSampleFactor) * verticalSampleFactor;
        }

        int samplesPerPixel = 1;

        tiffField = ifd.getField(TiffTag.SAMPLES_PER_PIXEL);
        if (tiffField != null) {
            samplesPerPixel = tiffField.getDataAsLong()[0];
        }

        int bitsPerSample = 1;

        tiffField = ifd.getField(TiffTag.BITS_PER_SAMPLE);
        if (tiffField != null) {
            bitsPerSample = tiffField.getDataAsLong()[0];
        }

        int tileWidth = -1;
        int tileLength = -1;

        TiffField<?> f_tileLength = ifd.getField(TiffTag.TILE_LENGTH);
        TiffField<?> f_tileWidth = ifd.getField(TiffTag.TILE_WIDTH);

        if (f_tileWidth != null) {
            tileWidth = f_tileWidth.getDataAsLong()[0];
            tileLength = f_tileLength.getDataAsLong()[0];
        }

        int rowsPerStrip = imageHeight;
        int rowWidth = imageWidth;

        TiffField<?> f_rowsPerStrip = ifd.getField(TiffTag.ROWS_PER_STRIP);
        if (f_rowsPerStrip != null) rowsPerStrip = f_rowsPerStrip.getDataAsLong()[0];

        if (rowsPerStrip > imageHeight) rowsPerStrip = imageHeight;

        if (tileWidth > 0) {
            rowsPerStrip = tileLength;
            rowWidth = tileWidth;
        }

        int planaryConfiguration = 1;

        tiffField = ifd.getField(TiffTag.PLANAR_CONFIGURATTION);
        if (tiffField != null) planaryConfiguration = tiffField.getDataAsLong()[0];

        int[] totalBytes2Read = new int[samplesPerPixel];

        if (planaryConfiguration == 1)
            totalBytes2Read[0] = ((rowWidth * bitsPerSample * samplesPerPixel + 7) / 8) * rowsPerStrip;
        else
            totalBytes2Read[0] = totalBytes2Read[1] = totalBytes2Read[2] = ((rowWidth * bitsPerSample + 7) / 8) * rowsPerStrip;

        if (photoMetric == TiffFieldEnum.PhotoMetric.YCbCr.getValue()) {
            if (samplesPerPixel != 3) samplesPerPixel = 3;

            int[] sampleBytesPerRow = new int[samplesPerPixel];
            sampleBytesPerRow[0] = (bitsPerSample * rowWidth + 7) / 8;
            sampleBytesPerRow[1] = (bitsPerSample * rowWidth / horizontalSampleFactor + 7) / 8;
            sampleBytesPerRow[2] = sampleBytesPerRow[1];

            int[] sampleRowsPerStrip = new int[samplesPerPixel];
            sampleRowsPerStrip[0] = rowsPerStrip;
            sampleRowsPerStrip[1] = rowsPerStrip / verticalSampleFactor;
            sampleRowsPerStrip[2] = sampleRowsPerStrip[1];

            totalBytes2Read[0] = sampleBytesPerRow[0] * sampleRowsPerStrip[0];
            totalBytes2Read[1] = sampleBytesPerRow[1] * sampleRowsPerStrip[1];
            totalBytes2Read[2] = totalBytes2Read[1];

            if (tiffField != null) planaryConfiguration = tiffField.getDataAsLong()[0];

            if (planaryConfiguration == 1)
                totalBytes2Read[0] = totalBytes2Read[0] + totalBytes2Read[1] + totalBytes2Read[2];
        }

        return totalBytes2Read;
    }

    public static void insertComments(List<String> comments, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        insertComments(comments, 0, rin, rout);
    }

    public static void insertComments(List<String> comments, int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        int offset = copyHeader(rin, rout);
        // Read the IFDs into a list first
        List<IFD> ifds = new ArrayList<IFD>();
        readIFDs(null, null, TiffTag.class, ifds, offset, rin);

        if (pageNumber < 0 || pageNumber >= ifds.size())
            throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));

        IFD workingPage = ifds.get(pageNumber);

        StringBuilder commentsBuilder = new StringBuilder();

        // ASCII field allows for multiple strings
        for (String comment : comments) {
            commentsBuilder.append(comment);
            commentsBuilder.append('\0');
        }

        workingPage.addField(new ASCIIField(TiffTag.IMAGE_DESCRIPTION.getValue(), commentsBuilder.toString()));

        offset = copyPages(ifds, offset, rin, rout);
        int firstIFDOffset = ifds.get(0).getStartOffset();

        writeToStream(rout, firstIFDOffset);
    }

    public static void insertExif(RandomAccessInputStream rin, RandomAccessOutputStream rout, Exif exif, boolean update) throws IOException {
        insertExif(rin, rout, exif, 0, update);
    }

    /**
     * Insert EXIF data with optional thumbnail IFD
     *
     * @param rin input image stream
     * @param rout output image stream
     * @param exif EXIF wrapper instance
     * @param pageNumber page offset where to insert EXIF (zero based)
     * @param update True to keep the original data, otherwise false
     * @throws Exception
     */
    public static void insertExif(RandomAccessInputStream rin, RandomAccessOutputStream rout, Exif exif, int pageNumber, boolean update) throws IOException {
        int offset = copyHeader(rin, rout);
        // Read the IFDs into a list first
        List<IFD> ifds = new ArrayList<IFD>();
        readIFDs(null, null, TiffTag.class, ifds, offset, rin);

        if (pageNumber < 0 || pageNumber >= ifds.size())
            throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));

        IFD imageIFD = ifds.get(pageNumber);
        IFD exifSubIFD = imageIFD.getChild(TiffTag.EXIF_SUB_IFD);
        IFD gpsSubIFD = imageIFD.getChild(TiffTag.GPS_SUB_IFD);
        IFD newImageIFD = exif.getImageIFD();
        IFD newExifSubIFD = exif.getExifIFD();
        IFD newGpsSubIFD = exif.getGPSIFD();

        if (newImageIFD != null) {
            Collection<TiffField<?>> fields = newImageIFD.getFields();
            for (TiffField<?> field : fields) {
                Tag tag = TiffTag.fromShort(field.getTag());
                if (imageIFD.getField(tag) != null && tag.isCritical())
                    throw new RuntimeException("Duplicate Tag: " + tag);
                imageIFD.addField(field);
            }
        }

        if (update && exifSubIFD != null && newExifSubIFD != null) {
            exifSubIFD.addFields(newExifSubIFD.getFields());
            newExifSubIFD = exifSubIFD;
        }

        if (newExifSubIFD != null) {
            imageIFD.addField(new LongField(TiffTag.EXIF_SUB_IFD.getValue(), new int[]{0})); // Place holder
            imageIFD.addChild(TiffTag.EXIF_SUB_IFD, newExifSubIFD);
        }

        if (update && gpsSubIFD != null && newGpsSubIFD != null) {
            gpsSubIFD.addFields(newGpsSubIFD.getFields());
            newGpsSubIFD = gpsSubIFD;
        }

        if (newGpsSubIFD != null) {
            imageIFD.addField(new LongField(TiffTag.GPS_SUB_IFD.getValue(), new int[]{0})); // Place holder
            imageIFD.addChild(TiffTag.GPS_SUB_IFD, newGpsSubIFD);
        }

        int writeOffset = FIRST_WRITE_OFFSET;
        // Copy pages
        writeOffset = copyPages(ifds, writeOffset, rin, rout);
        int firstIFDOffset = ifds.get(0).getStartOffset();

        writeToStream(rout, firstIFDOffset);
    }

    public static void insertICCProfile(byte[] icc_profile, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        insertICCProfile(icc_profile, 0, rin, rout);
    }

    /**
     * Insert ICC_Profile into TIFF page
     *
     * @param icc_profile byte array holding the ICC_Profile
     * @param pageNumber page offset where to insert ICC_Profile
     * @param rin RandomAccessInputStream for the input image
     * @param rout RandomAccessOutputStream for the output image
     * @throws Exception
     */
    public static void insertICCProfile(byte[] icc_profile, int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        int offset = copyHeader(rin, rout);
        // Read the IFDs into a list first
        List<IFD> ifds = new ArrayList<IFD>();
        readIFDs(null, null, TiffTag.class, ifds, offset, rin);

        if (pageNumber < 0 || pageNumber >= ifds.size())
            throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));

        IFD workingPage = ifds.get(pageNumber);
        workingPage.addField(new UndefinedField(TiffTag.ICC_PROFILE.getValue(), icc_profile));

        offset = copyPages(ifds, offset, rin, rout);
        int firstIFDOffset = ifds.get(0).getStartOffset();

        writeToStream(rout, firstIFDOffset);
    }

    public static void insertIPTC(RandomAccessInputStream rin, RandomAccessOutputStream rout, Collection<IPTCDataSet> iptcs, boolean update) throws IOException {
        insertIPTC(rin, rout, 0, iptcs, update);
    }

    /**
     * Insert IPTC data into TIFF image. If the original TIFF image contains IPTC data, we either keep
     * or override them depending on the input parameter "update."
     * <p>
     * There is a possibility that IPTC data presents in more than one places such as a normal TIFF
     * tag, or buried inside a Photoshop IPTC-NAA Image Resource Block (IRB), or even in a XMP block.
     * Currently this method does the following thing: if no IPTC data was found from both Photoshop or
     * normal IPTC tag, we insert the IPTC data with a normal IPTC tag. If IPTC data is found both as
     * a Photoshop tag and a normal IPTC tag, depending on the "update" parameter, we will either delete
     * the IPTC data from both places and insert the new IPTC data into the Photoshop tag or we will
     * synchronize the two sets of IPTC data, delete the original IPTC from both places and insert the
     * synchronized IPTC data along with the new IPTC data into the Photoshop tag. In both cases, we
     * will keep the other IRBs from the original Photoshop tag unchanged.
     *
     * @param rin RandomAccessInputStream for the original TIFF
     * @param rout RandomAccessOutputStream for the output TIFF with IPTC inserted
     * @param pageNumber page offset where to insert IPTC
     * @param iptcs A list of IPTCDataSet to insert into the TIFF image
     * @param update whether we want to keep the original IPTC data or override it
     *        completely new IPTC data set
     * @throws IOException
     */
    public static void insertIPTC(RandomAccessInputStream rin, RandomAccessOutputStream rout, int pageNumber, Collection<IPTCDataSet> iptcs, boolean update) throws IOException {
        int offset = copyHeader(rin, rout);
        // Read the IFDs into a list first
        List<IFD> ifds = new ArrayList<IFD>();
        readIFDs(null, null, TiffTag.class, ifds, offset, rin);

        if (pageNumber < 0 || pageNumber >= ifds.size())
            throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));

        IFD workingPage = ifds.get(pageNumber);

        ByteArrayOutputStream bout = new ByteArrayOutputStream();

        // See if we also have regular IPTC tag field
        TiffField<?> f_iptc = workingPage.removeField(TiffTag.IPTC);
        TiffField<?> f_photoshop = workingPage.getField(TiffTag.PHOTOSHOP);
        if (f_photoshop != null) { // Read 8BIMs
            IRB irb = new IRB((byte[]) f_photoshop.getData());
            // Shallow copy the map.
            Map<Short, _8BIM> bims = new HashMap<Short, _8BIM>(irb.get8BIM());
            _8BIM photoshop_iptc = bims.remove(ImageResourceID.IPTC_NAA.getValue());
            if (photoshop_iptc != null) { // If we have IPTC
                if (update) { // If we need to keep the old data, copy it
                    if (f_iptc != null) {// We are going to synchronize the two IPTC data
                        byte[] data = null;
                        if (f_iptc.getType() == FieldType.LONG)
                            data = ArrayUtils.toByteArray(f_iptc.getDataAsLong(), rin.getEndian() == IOUtils.BIG_ENDIAN);
                        else
                            data = (byte[]) f_iptc.getData();
                        copyIPTCDataSet(iptcs, data);
                    }
                    // Now copy the Photoshop IPTC data
                    copyIPTCDataSet(iptcs, photoshop_iptc.getData());
                    // Remove duplicates
                    iptcs = new ArrayList<IPTCDataSet>(new HashSet<IPTCDataSet>(iptcs));
                }
            }
            for (_8BIM bim : bims.values()) // Copy the other 8BIMs if any
                bim.write(bout);
            // Add a new Photoshop tag field to TIFF
            workingPage.addField(new UndefinedField(TiffTag.PHOTOSHOP.getValue(), bout.toByteArray()));
        } else { // We don't have photoshop, copy the old IPTC data in the IPTC tag is any
            if (f_iptc != null && update) {
                byte[] data = null;
                if (f_iptc.getType() == FieldType.LONG)
                    data = ArrayUtils.toByteArray(f_iptc.getDataAsLong(), rin.getEndian() == IOUtils.BIG_ENDIAN);
                else
                    data = (byte[]) f_iptc.getData();
                copyIPTCDataSet(iptcs, data);
            }
        }

        // Sort the IPTCDataSet collection
        List<IPTCDataSet> iptcList = new ArrayList<IPTCDataSet>(iptcs);
        Collections.sort(iptcList);
        // Write IPTCDataSet collection
        bout.reset();
        for (IPTCDataSet dataset : iptcList) {
            dataset.write(bout);
        }
        // Add IPTC to regular IPTC tag field
        workingPage.addField(new UndefinedField(TiffTag.IPTC.getValue(), bout.toByteArray()));

        offset = copyPages(ifds, offset, rin, rout);
        int firstIFDOffset = ifds.get(0).getStartOffset();

        writeToStream(rout, firstIFDOffset);
    }

    public static void insertIRB(RandomAccessInputStream rin, RandomAccessOutputStream rout, Collection<_8BIM> bims, boolean update) throws IOException {
        insertIRB(rin, rout, 0, bims, update);
    }

    public static void insertIRB(RandomAccessInputStream rin, RandomAccessOutputStream rout, int pageNumber, Collection<_8BIM> bims, boolean update) throws IOException {
        int offset = copyHeader(rin, rout);
        // Read the IFDs into a list first
        List<IFD> ifds = new ArrayList<IFD>();
        readIFDs(null, null, TiffTag.class, ifds, offset, rin);

        if (pageNumber < 0 || pageNumber >= ifds.size())
            throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));

        IFD workingPage = ifds.get(pageNumber);

        ByteArrayOutputStream bout = new ByteArrayOutputStream();

        if (update) {
            TiffField<?> f_irb = workingPage.getField(TiffTag.PHOTOSHOP);
            if (f_irb != null) {
                IRB irb = new IRB((byte[]) f_irb.getData());
                // Shallow copy the map.
                Map<Short, _8BIM> bimMap = new HashMap<Short, _8BIM>(irb.get8BIM());
                for (_8BIM bim : bims) // Replace the original data
                    bimMap.put(bim.getID(), bim);
                // In case we have two ThumbnailResource IRB, remove the Photoshop4.0 one
                if (bimMap.containsKey(ImageResourceID.THUMBNAIL_RESOURCE_PS4.getValue())
                        && bimMap.containsKey(ImageResourceID.THUMBNAIL_RESOURCE_PS5.getValue()))
                    bimMap.remove(ImageResourceID.THUMBNAIL_RESOURCE_PS4.getValue());
                bims = bimMap.values();
            }
        }

        for (_8BIM bim : bims)
            bim.write(bout);

        workingPage.addField(new UndefinedField(TiffTag.PHOTOSHOP.getValue(), bout.toByteArray()));

        offset = copyPages(ifds, offset, rin, rout);
        int firstIFDOffset = ifds.get(0).getStartOffset();

        writeToStream(rout, firstIFDOffset);
    }

    /**
     * Insert a thumbnail into PHOTOSHOP private tag field
     *
     * @param rin RandomAccessInputStream for the input TIFF
     * @param rout RandomAccessOutputStream for the output TIFF
     * @param thumbnail a Bitmap to be inserted
     * @throws Exception
     */
    public static void insertThumbnail(RandomAccessInputStream rin, RandomAccessOutputStream rout, BufferedImage thumbnail) throws IOException {
        // Sanity check
        if (thumbnail == null) throw new IllegalArgumentException("Input thumbnail is null");
        _8BIM bim = new ThumbnailResource(thumbnail);
        insertIRB(rin, rout, Arrays.asList(bim), true);
    }

    public static void insertXMP(XMP xmp, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        insertXMP(xmp.getData(), rin, rout);
    }

    public static void insertXMP(XMP xmp, int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        insertXMP(xmp.getData(), pageNumber, rin, rout);
    }

    public static void insertXMP(byte[] xmp, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        insertXMP(xmp, 0, rin, rout);
    }

    /**
     * Insert XMP data into TIFF image
     * @param xmp byte array for the XMP data to be inserted
     * @param pageNumber page offset where to insert XMP
     * @param rin RandomAccessInputStream for the input image
     * @param rout RandomAccessOutputStream for the output image
     * @throws IOException
     */
    public static void insertXMP(byte[] xmp, int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        int offset = copyHeader(rin, rout);
        // Read the IFDs into a list first
        List<IFD> ifds = new ArrayList<IFD>();
        readIFDs(null, null, TiffTag.class, ifds, offset, rin);

        if (pageNumber < 0 || pageNumber >= ifds.size())
            throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));

        IFD workingPage = ifds.get(pageNumber);
        workingPage.addField(new UndefinedField(TiffTag.XMP.getValue(), xmp));

        offset = copyPages(ifds, offset, rin, rout);
        int firstIFDOffset = ifds.get(0).getStartOffset();

        writeToStream(rout, firstIFDOffset);
    }

    public static void insertXMP(String xmp, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        Document doc = XMLUtils.createXML(xmp);
        XMLUtils.insertLeadingPI(doc, "xpacket", "begin='' id='W5M0MpCehiHzreSzNTczkc9d'");
        XMLUtils.insertTrailingPI(doc, "xpacket", "end='w'");
        byte[] xmpBytes = XMLUtils.serializeToByteArray(doc);
        insertXMP(xmpBytes, rin, rout);
    }

    public static void printIFDs(Collection<IFD> list, String indent) {
        int id = 0;
        LOGGER.info("Printing IFDs ... ");

        for (IFD currIFD : list) {
            LOGGER.info("IFD #{}", id);
            printIFD(currIFD, TiffTag.class, indent);
            id++;
        }
    }

    public static void printIFD(IFD currIFD, Class<? extends Tag> tagClass, String indent) {
        StringBuilder ifd = new StringBuilder();
        print(currIFD, tagClass, indent, ifd);
        LOGGER.info("\n{}", ifd);
    }

    private static void print(IFD currIFD, Class<? extends Tag> tagClass, String indent, StringBuilder ifds) {
        // Use reflection to invoke fromShort(short) method
        Method method = null;
        try {
            method = tagClass.getDeclaredMethod("fromShort", short.class);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        }
        Collection<TiffField<?>> fields = currIFD.getFields();
        int i = 0;

        for (TiffField<?> field : fields) {
            ifds.append(indent);
            ifds.append("Field #" + i + "\n");
            ifds.append(indent);
            short tag = field.getTag();
            Tag ftag = TiffTag.UNKNOWN;
            if (tag == ExifTag.PADDING.getValue()) {
                ftag = ExifTag.PADDING;
            } else {
                try {
                    ftag = (Tag) method.invoke(null, tag);
                } catch (IllegalAccessException e) {
                    LOGGER.error("IllegalAcessException", e);
                } catch (IllegalArgumentException e) {
                    LOGGER.error("IllegalArgumentException", e);
                } catch (InvocationTargetException e) {
                    LOGGER.error("InvocationTargetException", e);
                }
            }
            if (ftag == TiffTag.UNKNOWN) {
                LOGGER.warn("Tag: {} {}{}{} {}", ftag, "[Value: 0x", Integer.toHexString(tag & 0xffff), "]", "(Unknown)");
            } else {
                ifds.append("Tag: " + ftag + "\n");
            }
            FieldType ftype = field.getType();
            ifds.append(indent);
            ifds.append("Field type: " + ftype + "\n");
            int field_length = field.getLength();
            ifds.append(indent);
            ifds.append("Field length: " + field_length + "\n");
            ifds.append(indent);

            String suffix = null;
            if (ftype == FieldType.SHORT || ftype == FieldType.SSHORT)
                suffix = ftag.getFieldAsString(field.getDataAsLong());
            else
                suffix = ftag.getFieldAsString(field.getData());

            ifds.append("Field value: " + field.getDataAsString() + (StringUtils.isNullOrEmpty(suffix) ? "" : " => " + suffix) + "\n");

            i++;
        }

        Map<Tag, IFD> children = currIFD.getChildren();

        if (children.get(TiffTag.EXIF_SUB_IFD) != null) {
            ifds.append(indent + "--------- ");
            ifds.append("<<Exif SubIFD starts>>\n");
            print(children.get(TiffTag.EXIF_SUB_IFD), ExifTag.class, indent + "--------- ", ifds);
            ifds.append(indent + "--------- ");
            ifds.append("<<Exif SubIFD ends>>\n");
        }

        if (children.get(TiffTag.GPS_SUB_IFD) != null) {
            ifds.append(indent + "--------- ");
            ifds.append("<<GPS SubIFD starts>>\n");
            print(children.get(TiffTag.GPS_SUB_IFD), GPSTag.class, indent + "--------- ", ifds);
            ifds.append(indent + "--------- ");
            ifds.append("<<GPS SubIFD ends>>\n");
        }
    }

    private static int readHeader(RandomAccessInputStream rin) throws IOException {
        int offset = 0;
        // First 2 bytes determine the byte order of the file
        rin.seek(STREAM_HEAD);
        short endian = rin.readShort();
        offset += 2;

        if (endian == IOUtils.BIG_ENDIAN) {
            rin.setReadStrategy(ReadStrategyMM.getInstance());
        } else if (endian == IOUtils.LITTLE_ENDIAN) {
            rin.setReadStrategy(ReadStrategyII.getInstance());
        } else {
            rin.close();
            throw new RuntimeException("Invalid TIFF byte order");
        }

        // Read TIFF identifier
        rin.seek(offset);
        short tiff_id = rin.readShort();
        offset += 2;

        if (tiff_id != 0x2a) { //"*" 42 decimal
            rin.close();
            throw new RuntimeException("Invalid TIFF identifier");
        }

        rin.seek(offset);
        offset = rin.readInt();

        return offset;
    }

    private static int readIFD(IFD parent, Tag parentTag, Class<? extends Tag> tagClass, RandomAccessInputStream rin, List<IFD> list, int offset) throws IOException {
        // Use reflection to invoke fromShort(short) method
        Method method = null;
        try {
            method = tagClass.getDeclaredMethod("fromShort", short.class);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        }
        IFD tiffIFD = new IFD();
        rin.seek(offset);
        int no_of_fields = rin.readShort();
        offset += 2;

        for (int i = 0; i < no_of_fields; i++) {
            rin.seek(offset);
            short tag = rin.readShort();
            Tag ftag = TiffTag.UNKNOWN;
            try {
                ftag = (Tag) method.invoke(null, tag);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
            offset += 2;
            rin.seek(offset);
            short type = rin.readShort();
            FieldType ftype = FieldType.fromShort(type);
            offset += 2;
            rin.seek(offset);
            int field_length = rin.readInt();
            offset += 4;
            ////// Try to read actual data.
            switch (ftype) {
                case BYTE:
                case UNDEFINED:
                    byte[] data = new byte[field_length];
                    rin.seek(offset);
                    if (field_length <= 4) {
                        rin.readFully(data, 0, field_length);
                    } else {
                        rin.seek(rin.readInt());
                        rin.readFully(data, 0, field_length);
                    }
                    TiffField<byte[]> byteField = null;
                    if (ftype == FieldType.BYTE)
                        byteField = new ByteField(tag, data);
                    else
                        byteField = new UndefinedField(tag, data);
                    tiffIFD.addField(byteField);
                    offset += 4;
                    break;
                case ASCII:
                    data = new byte[field_length];
                    if (field_length <= 4) {
                        rin.seek(offset);
                        rin.readFully(data, 0, field_length);
                    } else {
                        rin.seek(offset);
                        rin.seek(rin.readInt());
                        rin.readFully(data, 0, field_length);
                    }
                    TiffField<String> ascIIField = new ASCIIField(tag, new String(data, StandardCharsets.UTF_8));
                    tiffIFD.addField(ascIIField);
                    offset += 4;
                    break;
                case SHORT:
                    short[] sdata = new short[field_length];
                    if (field_length == 1) {
                        rin.seek(offset);
                        sdata[0] = rin.readShort();
                        offset += 4;
                    } else if (field_length == 2) {
                        rin.seek(offset);
                        sdata[0] = rin.readShort();
                        offset += 2;
                        rin.seek(offset);
                        sdata[1] = rin.readShort();
                        offset += 2;
                    } else {
                        rin.seek(offset);
                        int toOffset = rin.readInt();
                        offset += 4;
                        for (int j = 0; j < field_length; j++) {
                            rin.seek(toOffset);
                            sdata[j] = rin.readShort();
                            toOffset += 2;
                        }
                    }
                    TiffField<short[]> shortField = new ShortField(tag, sdata);
                    tiffIFD.addField(shortField);
                    break;
                case LONG:
                    int[] ldata = new int[field_length];
                    if (field_length == 1) {
                        rin.seek(offset);
                        ldata[0] = rin.readInt();
                        offset += 4;
                    } else {
                        rin.seek(offset);
                        int toOffset = rin.readInt();
                        offset += 4;
                        for (int j = 0; j < field_length; j++) {
                            rin.seek(toOffset);
                            ldata[j] = rin.readInt();
                            toOffset += 4;
                        }
                    }
                    TiffField<int[]> longField = new LongField(tag, ldata);
                    tiffIFD.addField(longField);

                    if ((ftag == TiffTag.EXIF_SUB_IFD) && (ldata[0] != 0)) {
                        try { // If something bad happens, we skip the sub IFD
                            readIFD(tiffIFD, TiffTag.EXIF_SUB_IFD, ExifTag.class, rin, null, ldata[0]);
                        } catch (Exception e) {
                            tiffIFD.removeField(TiffTag.EXIF_SUB_IFD);
                            e.printStackTrace();
                        }
                    } else if ((ftag == TiffTag.GPS_SUB_IFD) && (ldata[0] != 0)) {
                        try {
                            readIFD(tiffIFD, TiffTag.GPS_SUB_IFD, GPSTag.class, rin, null, ldata[0]);
                        } catch (Exception e) {
                            tiffIFD.removeField(TiffTag.GPS_SUB_IFD);
                            e.printStackTrace();
                        }
                    } else if ((ftag == ExifTag.EXIF_INTEROPERABILITY_OFFSET) && (ldata[0] != 0)) {
                        try {
                            readIFD(tiffIFD, ExifTag.EXIF_INTEROPERABILITY_OFFSET, InteropTag.class, rin, null, ldata[0]);
                        } catch (Exception e) {
                            tiffIFD.removeField(ExifTag.EXIF_INTEROPERABILITY_OFFSET);
                            e.printStackTrace();
                        }
                    } else if (ftag == TiffTag.SUB_IFDS) {
                        for (int ifd = 0; ifd < ldata.length; ifd++) {
                            try {
                                readIFD(tiffIFD, TiffTag.SUB_IFDS, TiffTag.class, rin, null, ldata[0]);
                            } catch (Exception e) {
                                tiffIFD.removeField(TiffTag.SUB_IFDS);
                                e.printStackTrace();
                            }
                        }
                    }
                    break;
                case FLOAT:
                    float[] fdata = new float[field_length];
                    if (field_length == 1) {
                        rin.seek(offset);
                        fdata[0] = rin.readFloat();
                        offset += 4;
                    } else {
                        rin.seek(offset);
                        int toOffset = rin.readInt();
                        offset += 4;
                        for (int j = 0; j < field_length; j++) {
                            rin.seek(toOffset);
                            fdata[j] = rin.readFloat();
                            toOffset += 4;
                        }
                    }
                    TiffField<float[]> floatField = new FloatField(tag, fdata);
                    tiffIFD.addField(floatField);

                    break;
                case DOUBLE:
                    double[] ddata = new double[field_length];
                    rin.seek(offset);
                    int toOffset = rin.readInt();
                    offset += 4;
                    for (int j = 0; j < field_length; j++) {
                        rin.seek(toOffset);
                        ddata[j] = rin.readDouble();
                        toOffset += 8;
                    }
                    TiffField<double[]> doubleField = new DoubleField(tag, ddata);
                    tiffIFD.addField(doubleField);

                    break;
                case RATIONAL:
                case SRATIONAL:
                    int len = 2 * field_length;
                    ldata = new int[len];
                    rin.seek(offset);
                    toOffset = rin.readInt();
                    offset += 4;
                    for (int j = 0; j < len; j += 2) {
                        rin.seek(toOffset);
                        ldata[j] = rin.readInt();
                        toOffset += 4;
                        rin.seek(toOffset);
                        ldata[j + 1] = rin.readInt();
                        toOffset += 4;
                    }
                    TiffField<int[]> rationalField = null;
                    if (ftype == FieldType.SRATIONAL) {
                        rationalField = new SRationalField(tag, ldata);
                    } else {
                        rationalField = new RationalField(tag, ldata);
                    }
                    tiffIFD.addField(rationalField);

                    break;
                case IFD:
                    ldata = new int[field_length];
                    if (field_length == 1) {
                        rin.seek(offset);
                        ldata[0] = rin.readInt();
                        offset += 4;
                    } else {
                        rin.seek(offset);
                        toOffset = rin.readInt();
                        offset += 4;
                        for (int j = 0; j < field_length; j++) {
                            rin.seek(toOffset);
                            ldata[j] = rin.readInt();
                            toOffset += 4;
                        }
                    }
                    TiffField<int[]> ifdField = new IFDField(tag, ldata);
                    tiffIFD.addField(ifdField);
                    for (int ifd = 0; ifd < ldata.length; ifd++) {
                        readIFD(tiffIFD, TiffTag.SUB_IFDS, TiffTag.class, rin, null, ldata[0]);
                    }

                    break;
                default:
                    offset += 4;
                    break;
            }
        }
        // If this is a child IFD, add it to its parent
        if (parent != null)
            parent.addChild(parentTag, tiffIFD);
        else // Otherwise, add to the main IFD list
            list.add(tiffIFD);
        rin.seek(offset);

        return rin.readInt();
    }

    private static void readIFDs(IFD parent, Tag parentTag, Class<? extends Tag> tagClass, List<IFD> list, int offset, RandomAccessInputStream rin) throws IOException {
        // Read the IFDs into a list first
        while (offset != 0) {
            offset = readIFD(parent, parentTag, tagClass, rin, list, offset);
        }
    }

    public static void readIFDs(List<IFD> list, RandomAccessInputStream rin) throws IOException {
        int offset = readHeader(rin);
        readIFDs(null, null, TiffTag.class, list, offset, rin);
    }

    public static Map<MetadataType, Metadata> readMetadata(RandomAccessInputStream rin) throws IOException {
        return readMetadata(rin, 0);
    }

    public static Map<MetadataType, Metadata> readMetadata(RandomAccessInputStream rin, int pageNumber) throws IOException {
        Map<MetadataType, Metadata> metadataMap = new HashMap<MetadataType, Metadata>();

        int offset = readHeader(rin);
        List<IFD> ifds = new ArrayList<IFD>();
        readIFDs(null, null, TiffTag.class, ifds, offset, rin);

        if (pageNumber < 0 || pageNumber >= ifds.size())
            throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));

        IFD currIFD = ifds.get(pageNumber);
        TiffField<?> field = currIFD.getField(TiffTag.ICC_PROFILE);
        if (field != null) { // We have found ICC_Profile
            metadataMap.put(MetadataType.ICC_PROFILE, new ICCProfile((byte[]) field.getData()));
        }
        field = currIFD.getField(TiffTag.XMP);
        if (field != null) { // We have found XMP
            metadataMap.put(MetadataType.XMP, new TiffXMP((byte[]) field.getData()));
        }
        field = currIFD.getField(TiffTag.PHOTOSHOP);
        if (field != null) { // We have found Photoshop IRB
            IRB irb = new IRB((byte[]) field.getData());
            metadataMap.put(MetadataType.PHOTOSHOP_IRB, irb);
            _8BIM photoshop_8bim = irb.get8BIM(ImageResourceID.IPTC_NAA.getValue());
            if (photoshop_8bim != null) { // If we have IPTC data inside Photoshop, keep it
                IPTC iptc = new IPTC(photoshop_8bim.getData());
                metadataMap.put(MetadataType.IPTC, iptc);
            }
        }
        field = currIFD.getField(TiffTag.IPTC);
        if (field != null) { // We have found IPTC data
            IPTC iptc = (IPTC) (metadataMap.get(MetadataType.IPTC));
            byte[] iptcData = null;
            FieldType type = field.getType();
            if (type == FieldType.LONG)
                iptcData = ArrayUtils.toByteArray(field.getDataAsLong(), rin.getEndian() == IOUtils.BIG_ENDIAN);
            else
                iptcData = (byte[]) field.getData();
            if (iptc != null) // If we have IPTC data from IRB, consolidate it with the current data
                iptcData = ArrayUtils.concat(iptcData, iptc.getData());
            metadataMap.put(MetadataType.IPTC, new IPTC(iptcData));
        }
        field = currIFD.getField(TiffTag.EXIF_SUB_IFD);
        if (field != null) { // We have found EXIF SubIFD
            metadataMap.put(MetadataType.EXIF, new TiffExif(currIFD));
        }
        field = currIFD.getField(TiffTag.IMAGE_SOURCE_DATA);
        if (field != null) {
            boolean bigEndian = (rin.getEndian() == IOUtils.BIG_ENDIAN);
            ReadStrategy readStrategy = bigEndian ? ReadStrategyMM.getInstance() : ReadStrategyII.getInstance();
            metadataMap.put(MetadataType.PHOTOSHOP_DDB, new DDB((byte[]) field.getData(), readStrategy));
        }
        field = currIFD.getField(TiffTag.IMAGE_DESCRIPTION);
        if (field != null) { // We have Comment
            Comments comments = new Comments();
            comments.addComment(field.getDataAsString());
            metadataMap.put(MetadataType.COMMENT, comments);
        }

        return metadataMap;
    }

    /**
     * Remove meta data from TIFF image
     *
     * @param rin RandomAccessInputStream for the input image
     * @param rout RandomAccessOutputStream for the output image
     * @param pageNumber working page from which to remove metadata
     * @param metadataTypes a variable length array of MetadataType to be removed
     * @throws IOException
     * @return A map of the removed metadata
     */
    public static Map<MetadataType, Metadata> removeMetadata(int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout, MetadataType... metadataTypes) throws IOException {
        return removeMetadata(new HashSet<MetadataType>(Arrays.asList(metadataTypes)), pageNumber, rin, rout);
    }

    /**
     * Remove meta data from TIFF image
     *
     * @param rin RandomAccessInputStream for the input image
     * @param rout RandomAccessOutputStream for the output image
     * @param metadataTypes a variable length array of MetadataType to be removed
     * @throws IOException
     * @return A map of the removed metadata
     */
    public static Map<MetadataType, Metadata> removeMetadata(RandomAccessInputStream rin, RandomAccessOutputStream rout, MetadataType... metadataTypes) throws IOException {
        return removeMetadata(0, rin, rout, metadataTypes);
    }

    /**
     * Remove meta data from TIFF image
     *
     * @param pageNumber working page from which to remove EXIF and GPS data
     * @param rin RandomAccessInputStream for the input image
     * @param rout RandomAccessOutputStream for the output image
     * @throws IOException
     * @return A map of the removed metadata
     */
    public static Map<MetadataType, Metadata> removeMetadata(Set<MetadataType> metadataTypes, int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        int offset = copyHeader(rin, rout);
        // Read the IFDs into a list first
        List<IFD> ifds = new ArrayList<IFD>();
        readIFDs(null, null, TiffTag.class, ifds, offset, rin);

        // Create a map to hold all the metadata and thumbnails
        Map<MetadataType, Metadata> metadataMap = new HashMap<MetadataType, Metadata>();

        if (pageNumber < 0 || pageNumber >= ifds.size())
            throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));

        IFD workingPage = ifds.get(pageNumber);

        TiffField<?> metadata = null;

        for (MetadataType metaType : metadataTypes) {
            switch (metaType) {
                case XMP:
                    TiffField<?> xmpField = workingPage.removeField(TiffTag.XMP);
                    if (xmpField != null) metadataMap.put(MetadataType.XMP, new TiffXMP((byte[]) xmpField.getData()));
                    metadata = workingPage.removeField(TiffTag.PHOTOSHOP);
                    if (metadata != null) {
                        byte[] data = (byte[]) metadata.getData();
                        // We only remove XMP and keep the other IRB data untouched.
                        List<_8BIM> bims = removeMetadataFromIRB(workingPage, data, ImageResourceID.XMP_METADATA);
                        if (bims.size() > 0 && xmpField == null)
                            metadataMap.put(MetadataType.XMP, new TiffXMP(bims.get(0).getData()));
                    }
                    break;
                case IPTC:
                    TiffField<?> iptcField = workingPage.removeField(TiffTag.IPTC);
                    if (iptcField != null) metadataMap.put(MetadataType.IPTC, new IPTC((byte[]) iptcField.getData()));
                    metadata = workingPage.removeField(TiffTag.PHOTOSHOP);
                    if (metadata != null) {
                        byte[] data = (byte[]) metadata.getData();
                        // We only remove IPTC_NAA and keep the other IRB data untouched.
                        List<_8BIM> bims = removeMetadataFromIRB(workingPage, data, ImageResourceID.IPTC_NAA);
                        if (bims.size() > 0) {
                            // See if we already have IPTC data
                            IPTC iptc = (IPTC) (metadataMap.remove(MetadataType.IPTC));
                            if (iptc != null) { // Consolidate them together
                                byte[] iptcData = ArrayUtils.concat(iptc.getData(), bims.get(0).getData());
                                metadataMap.put(MetadataType.IPTC, new IPTC(iptcData));
                            } else { // Otherwise, add the data from IRB
                                metadataMap.put(MetadataType.IPTC, new IPTC(bims.get(0).getData()));
                            }
                        }
                    }
                    break;
                case ICC_PROFILE:
                    TiffField<?> iccField = workingPage.removeField(TiffTag.ICC_PROFILE);
                    if (iccField != null)
                        metadataMap.put(MetadataType.ICC_PROFILE, new ICCProfile((byte[]) iccField.getData()));
                    metadata = workingPage.removeField(TiffTag.PHOTOSHOP);
                    if (metadata != null) {
                        byte[] data = (byte[]) metadata.getData();
                        // We only remove ICC_PROFILE and keep the other IRB data untouched.
                        List<_8BIM> bims = removeMetadataFromIRB(workingPage, data, ImageResourceID.ICC_PROFILE);
                        if (bims.size() > 0 && iccField == null)
                            metadataMap.put(MetadataType.ICC_PROFILE, new ICCProfile(bims.get(0).getData()));
                    }
                    break;
                case PHOTOSHOP_IRB:
                    TiffField<?> irbField = workingPage.removeField(TiffTag.PHOTOSHOP);
                    if (irbField != null)
                        metadataMap.put(MetadataType.PHOTOSHOP_IRB, new IRB((byte[]) irbField.getData()));
                    break;
                case EXIF:
                    TiffField<?> exifField = workingPage.removeField(TiffTag.EXIF_SUB_IFD);
                    if (exifField != null) metadataMap.put(MetadataType.EXIF, new TiffExif(workingPage));
                    workingPage.removeField(TiffTag.GPS_SUB_IFD);
                    metadata = workingPage.removeField(TiffTag.PHOTOSHOP);
                    if (metadata != null) {
                        byte[] data = (byte[]) metadata.getData();
                        // We only remove EXIF and keep the other IRB data untouched.
                        List<_8BIM> bims = removeMetadataFromIRB(workingPage, data, ImageResourceID.EXIF_DATA1, ImageResourceID.EXIF_DATA3);
                        if (exifField == null && bims.size() > 0) {
                            // Read the EXIF data
                            RandomAccessInputStream exif = new com.symaster.common.pixy.io.MemoryCacheRandomAccessInputStream(new ByteArrayInputStream(bims.get(0).getData()));
                            List<IFD> exifIFDs = new ArrayList<IFD>();
                            readIFDs(exifIFDs, exif);
                            exif.close();
                            // put the data into metadataMap
                            if (exifIFDs.size() > 0) metadataMap.put(MetadataType.EXIF, new TiffExif(exifIFDs.get(0)));
                        }
                    }
                    break;
                case COMMENT:
                    TiffField<?> commentField = workingPage.removeField(TiffTag.IMAGE_DESCRIPTION);
                    if (commentField != null) {
                        Comments comments = new Comments();
                        comments.addComment(commentField.getDataAsString());
                        metadataMap.put(MetadataType.COMMENT, comments);
                    }
                    break;
                default:
            }
        }

        offset = copyPages(ifds, offset, rin, rout);
        int firstIFDOffset = ifds.get(0).getStartOffset();

        writeToStream(rout, firstIFDOffset);

        return metadataMap;
    }

    /**
     * Remove meta data from TIFF image
     *
     * @param metadataTypes a set of MetadataType to be removed
     * @param rin RandomAccessInputStream for the input image
     * @param rout RandomAccessOutputStream for the output image
     * @throws IOException
     * @return A map of the removed metadata
     */
    public static Map<MetadataType, Metadata> removeMetadata(Set<MetadataType> metadataTypes, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        return removeMetadata(metadataTypes, 0, rin, rout);
    }

    private static List<_8BIM> removeMetadataFromIRB(IFD workingPage, byte[] data, ImageResourceID... ids) throws IOException {
        IRB irb = new IRB(data);
        // Shallow copy the map.
        Map<Short, _8BIM> bimMap = new HashMap<Short, _8BIM>(irb.get8BIM());
        List<_8BIM> bimList = new ArrayList<_8BIM>();
        // We only remove XMP and keep the other IRB data untouched.
        for (ImageResourceID id : ids) {
            _8BIM bim = bimMap.remove(id.getValue());
            if (bim != null) bimList.add(bim);
        }
        if (bimMap.size() > 0) {
            // Write back the IRB
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            for (_8BIM bim : bimMap.values())
                bim.write(bout);
            // Add new PHOTOSHOP field
            workingPage.addField(new ByteField(TiffTag.PHOTOSHOP.getValue(), bout.toByteArray()));
        }

        return bimList;
    }

    public static int retainPages(int startPage, int endPage, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
        if (startPage < 0 || endPage < 0)
            throw new IllegalArgumentException("Negative start or end page");
        else if (startPage > endPage)
            throw new IllegalArgumentException("Start page is larger than end page");

        List<IFD> list = new ArrayList<IFD>();

        int offset = copyHeader(rin, rout);

        // Step 1: read the IFDs into a list first
        readIFDs(null, null, TiffTag.class, list, offset, rin);
        // Step 2: remove pages from a multiple page TIFF
        int pagesRetained = list.size();
        List<IFD> newList = new ArrayList<IFD>();
        if (startPage <= list.size() - 1) {
            if (endPage > list.size() - 1) endPage = list.size() - 1;
            for (int i = endPage; i >= startPage; i--) {
                newList.add(list.get(i));
            }
        }
        if (newList.size() > 0) {
            pagesRetained = newList.size();
            list.retainAll(newList);
        }
        // Reset pageNumber for the existing pages
        for (int i = 0; i < list.size(); i++) {
            list.get(i).removeField(TiffTag.PAGE_NUMBER);
            list.get(i).addField(new ShortField(TiffTag.PAGE_NUMBER.getValue(), new short[]{(short) i, (short) (list.size() - 1)}));
        }
        // End of removing pages
        // Step 3: copy the remaining pages
        // 0x08 is the first write offset
        int writeOffset = FIRST_WRITE_OFFSET;
        offset = copyPages(list, writeOffset, rin, rout);
        int firstIFDOffset = list.get(0).getStartOffset();

        writeToStream(rout, firstIFDOffset);

        return pagesRetained;
    }

    // Return number of pages retained
    public static int retainPages(RandomAccessInputStream rin, RandomAccessOutputStream rout, int... pages) throws IOException {
        List<IFD> list = new ArrayList<IFD>();

        int offset = copyHeader(rin, rout);
        // Step 1: read the IFDs into a list first
        readIFDs(null, null, TiffTag.class, list, offset, rin);
        // Step 2: remove pages from a multiple page TIFF
        int pagesRetained = list.size();
        List<IFD> newList = new ArrayList<IFD>();
        Arrays.sort(pages);
        for (int i = pages.length - 1; i >= 0; i--) {
            if (pages[i] >= 0 && pages[i] < list.size())
                newList.add(list.get(pages[i]));
        }
        if (newList.size() > 0) {
            pagesRetained = newList.size();
            list.retainAll(newList);
        }
        // End of removing pages
        // Reset pageNumber for the existing pages
        for (int i = 0; i < list.size(); i++) {
            list.get(i).removeField(TiffTag.PAGE_NUMBER);
            list.get(i).addField(new ShortField(TiffTag.PAGE_NUMBER.getValue(), new short[]{(short) i, (short) (list.size() - 1)}));
        }
        // Step 3: copy the remaining pages
        // 0x08 is the first write offset
        int writeOffset = FIRST_WRITE_OFFSET;
        offset = copyPages(list, writeOffset, rin, rout);
        int firstIFDOffset = list.get(0).getStartOffset();

        writeToStream(rout, firstIFDOffset);

        return pagesRetained;
    }

    public static void write(TIFFImage tiffImage, RandomAccessOutputStream rout) throws IOException {
        RandomAccessInputStream rin = tiffImage.getInputStream();
        int offset = writeHeader(rout);
        offset = copyPages(tiffImage.getIFDs(), offset, rin, rout);
        int firstIFDOffset = tiffImage.getIFDs().get(0).getStartOffset();

        writeToStream(rout, firstIFDOffset);
    }

    // Return stream offset where to write actual image data or IFD
    private static int writeHeader(RandomAccessOutputStream rout) throws IOException {
        // Write byte order
        short endian = rout.getEndian();
        rout.writeShort(endian);
        // Write TIFF identifier
        rout.writeShort(0x2a);

        return FIRST_WRITE_OFFSET;
    }

    private static void writeToStream(RandomAccessOutputStream rout, int firstIFDOffset) throws IOException {
        // Go to the place where we should write the first IFD offset
        // and write the first IFD offset
        rout.seek(OFFSET_TO_WRITE_FIRST_IFD_OFFSET);
        rout.writeInt(firstIFDOffset);
        // Dump the data to the real output stream
        rout.seek(STREAM_HEAD);
        rout.writeToStream(rout.getLength());
        // rout.flush();
    }
}
